package com.google.code.multitester.testers;

import com.google.code.multitester.annonations.ImportedTest;
import com.google.code.multitester.annonations.processors.ExportedFieldsFinder;
import com.google.code.multitester.annonations.processors.ExportedMethodsFinder;
import com.google.code.multitester.annonations.processors.FieldValuePointer;
import com.google.code.multitester.annonations.processors.VoidMethodPointer;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author Yuriy Sechko
 */
public class MultiTester {
    private final ImportedTest configuration;
    private final Object testClassInstance;
    private final Map<String, VoidMethodPointer> allExportedMethods;

    /**
     * Reads configuration data from given configuration class, creates new
     * instance of test class and looks for all methods which aliases are
     * presented in startup and shutdown sequences.
     * <p/>
     * <p/>
     * <ul>
     * <li>configuration class in null</li>
     * <li>configuration class is not marked with
     * {@link com.google.code.multitester.annonations.ImportedTest}
     * annotation</li>
     * <li>empty alias in the startup sequence encountered</li>
     * <li>empty alias in the shutdown sequence encountered</li>
     * <li>duplicated aliases in the startup sequence encountered</li>
     * <li>duplicated aliases in the shutdown sequence encountered</li>
     * </ul>
     * <p/>
     * when new instance of test class cannot be created
     *
     * @param classContainingConfiguration contains information about test class used as skeleton for
     *                                     certain test case. Class containing configuration should be
     *                                     marked with {@link ImportedTest}
     */
    public MultiTester(final Class classContainingConfiguration) {
        Checkings.configurationIsNull(classContainingConfiguration);
        final Annotation importedTestAnnotation = classContainingConfiguration
                .getAnnotation(ImportedTest.class);
        Checkings.isImportedTestAnnotationIsPresented(importedTestAnnotation);
        final ImportedTest actualConfig = (ImportedTest) importedTestAnnotation;
        Checkings.emptyAliasesInStartupSequence(actualConfig);
        Checkings.emptyAliasesInShutdownSequence(actualConfig);
        Checkings.duplicatedAliasesInStartupSequence(actualConfig);
        Checkings.duplicatedAliasesInShutdownSequence(actualConfig);
        this.configuration = actualConfig;
        this.testClassInstance = newInstanceOfTestClass(this.configuration);
        this.allExportedMethods = new ExportedMethodsFinder(
                this.testClassInstance).find();
    }

    private Object newInstanceOfTestClass(final ImportedTest configuration) {
        try {
            final Object newInstance = configuration.testClass().newInstance();
            return newInstance;
        } catch (InstantiationException instantiationException) {
            final String errorMessage = String
                    .format("Cannot create instance of class % using default constructor.",
                            this.configuration.testClass().getCanonicalName());
            throw new IllegalStateException(errorMessage,
                    instantiationException);
        } catch (IllegalAccessException accessException) {
            final String errorMessage = String
                    .format("Cannot access default contructo of class % using default constructor.",
                            this.configuration.testClass().getCanonicalName());
            throw new IllegalStateException(errorMessage, accessException);
        }
    }

    /**
     * Calls all methods which annotated with
     * {@link com.google.code.multitester.annonations.Exported} and if their
     * aliases is presented in {@see ImportedTest#startupSequence} attribute of
     * configuration class.
     * <p/>
     * Methods are called on the instance of test class created within {@see
     * MultiTester}'s constructor. Methods are called exactly in the order as
     * specified in startup sequence.
     *
     * @throws java.lang.IllegalStateException when method with particular alias is not found in specified
     *                                         test class
     */
    public void startup() {

        for (final String currentStartupMethodAlias : configuration
                .startupSequence()) {
            final VoidMethodPointer currentStartupMethod = allExportedMethods
                    .get(currentStartupMethodAlias);
            if (currentStartupMethod == null) {
                final String errorMessage = String
                        .format("Cannot process startup sequence: exported method declared with alias '%s' does not exists in test class '%s'.",
                                currentStartupMethodAlias, testClassInstance
                                        .getClass().getCanonicalName());
                throw new IllegalStateException(errorMessage);
            }
            currentStartupMethod.invoke();
        }
    }

    /**
     * Similar to {@see MultiTester#startup}.
     *
     * @throws java.lang.IllegalStateException when method with particular alias is not found in specified
     *                                         test class
     */

    public void shutdown() {
        for (final String currentShutdownMethodAlias : configuration
                .shutdownSequence()) {
            final VoidMethodPointer currentStartupMethod = allExportedMethods
                    .get(currentShutdownMethodAlias);
            if (currentStartupMethod == null) {
                final String errorMessage = String
                        .format("Cannot process shutdown sequence: exported method declared with alias '%s' does not exists in test class '%s'.",
                                currentShutdownMethodAlias, testClassInstance
                                        .getClass().getCanonicalName());
                throw new IllegalStateException(errorMessage);
            }
            currentStartupMethod.invoke();
        }
    }

    /**
     * Finds field marked as
     * {@link com.google.code.multitester.annonations.Exported} with given
     * alias, gets its value and casts it to the specified type.
     * <p/>
     * The same field can be returned arbitrary times with different type (in
     * case when field is a class that implement more than one interface).
     *
     * @param alias     string alias of the desired field. Should be provided within
     *                  {@link com.google.code.multitester.annonations.Exported}
     *                  annotation
     * @param fieldType type to cast to
     * @return value of the field casted to specified type. If field was not
     * found in test class then exception will be thrown.
     * @throws java.lang.IllegalArgumentException when:
     *                                            <ul>
     *                                            <li>given alias is null</li> <li>given field type is null
     *                                            </li> <li>given alias is empty</li>
     *                                            </ul>
     * @throws java.lang.IllegalStateException    when exported field with given alias was not found in the
     *                                            instance of test class
     */
    public <T> T importField(final String alias, final Class<T> fieldType) {
        Checkings.aliasIsNull(alias);
        Checkings.fieldTypeIsNull(fieldType);
        Checkings.aliasIsEmpty(alias);
        final Map<String, FieldValuePointer> exportedFieldsFound = new ExportedFieldsFinder(
                testClassInstance).find();
        final FieldValuePointer fieldMarkedWithGiveAlias = exportedFieldsFound
                .get(alias);
        if (fieldMarkedWithGiveAlias == null) {
            final String errorMessage = String
                    .format("Field exported with alias '%s' not found in test class %s.",
                            alias, testClassInstance.getClass()
                                    .getCanonicalName());
            throw new IllegalStateException(errorMessage);
        }
        return fieldMarkedWithGiveAlias.value(fieldType);
    }

    private static class Checkings {
        public static void aliasIsNull(final String alias) {
            if (alias == null)
                throw new IllegalArgumentException("Alias should not be null.");
        }

        public static <T> void fieldTypeIsNull(final Class<T> fieldType) {
            if (fieldType == null)
                throw new IllegalArgumentException(
                        "Type of the field should not be null.");
        }

        public static void aliasIsEmpty(final String alias) {
            if ("".equals(alias))
                throw new IllegalArgumentException("Alias should not be empty.");
        }

        public static void duplicatedAliasesInShutdownSequence(
                ImportedTest actualConfig) {
            final Set<String> shutdownSequenceAliases = new HashSet<String>();
            for (final String currentAlias : actualConfig.shutdownSequence()) {
                if (shutdownSequenceAliases.contains(currentAlias))
                    throw new IllegalArgumentException(
                            "Duplicated aliases are not allowed in shutdown sequence.");
                shutdownSequenceAliases.add(currentAlias);
            }
        }

        public static void duplicatedAliasesInStartupSequence(
                ImportedTest actualConfig) {
            final Set<String> startupSequenceAliases = new HashSet<String>();
            for (final String currentAlias : actualConfig.startupSequence()) {
                if (startupSequenceAliases.contains(currentAlias))
                    throw new IllegalArgumentException(
                            "Duplicated aliases are not allowed in startup sequence.");
                startupSequenceAliases.add(currentAlias);
            }
        }

        public static void emptyAliasesInShutdownSequence(
                ImportedTest actualConfig) {
            for (final String currentAlias : actualConfig.shutdownSequence()) {

                if ("".equals(currentAlias))
                    throw new IllegalArgumentException(
                            "Empty aliases are not allowed in shutdown sequence.");
            }
        }

        public static void emptyAliasesInStartupSequence(
                ImportedTest actualConfig) {
            for (final String currentAlias : actualConfig.startupSequence()) {
                if ("".equals(currentAlias))
                    throw new IllegalArgumentException(
                            "Empty aliases are not allowed in startup sequence.");
            }
        }

        public static void isImportedTestAnnotationIsPresented(
                final Annotation importedTestAnnotation) {
            if (importedTestAnnotation == null)
                throw new IllegalArgumentException(
                        "Given configuration class does not contain 'ImportedTest' annotation.");
        }

        public static void configurationIsNull(Class configuration) {
            if (configuration == null)
                throw new IllegalArgumentException(
                        "Configuration class should not be null.");
        }
    }
}
